Pangeo Showcase: "Ensemble Weather Forecast Data Streaming with Kerchunk / VirtualiZarr" (November 12, 2025 at 12 PM ET)

Title: “Ensemble Weather Forecast Data Streaming with Kerchunk / VirtualiZarr”
Invited Speaker: Nishadh Kalladath

When: Wednesday, November 12, 2025 at 12 PM EST (2025-11-12T17:00:00Z)
Where: Launch Meeting - Zoom
Abstract:

The role of Ensemble Prediction Systems (EPS) in risk assessment and impact-based forecasting is central to modern early warning systems. By combining extreme value analysis on long-term observations to define hazard thresholds and assessing whether ensemble forecast probabilities exceed them, decision-makers can continuously monitor evolving risks in near real time. Under the CRAF’D-funded initiative at ICPAC, Kenya, this work advances the use of cloud-optimized datasets for forecast-to-impact assessments, emphasizing the need for efficient data streaming and scalable analysis.

Kerchunk and VirtualiZarr enable seamless, on-demand access to multi-year GRIB datasets stored in cloud object storage such as AWS S3 open data registry, transforming them into virtual Zarr datasets without duplication or download. This approach is critical for regional services in East Africa that depend on timely access to GEFS and ECMWF forecast data for anticipatory action.

This presentation showcases methods to parse GEFS and ECMWF datasets in AWS S3, utilize GRIB index files to reduce the need for full GRIB scans, and address challenges in transitioning from Zarr version 2 to 3—particularly around metadata and codec compatibility in large-scale ensemble data processing. Insights gained from the Zarr Summit were instrumental in adopting version 3 features and developing a forecast-specific GRIB parser in Kerchunk for integration with VirtualiZarr.

Agenda:

  • ~15 minutes - Showcase presentation
  • 10 - 30 minutes - Discussion
  • 15 - 30 minutes - Community check-in
2 Likes

Australia will be asleep but very much looking forward to watching the recording. :slightly_smiling_face:

2 Likes

same in New Zealand :slight_smile:

1 Like

I actually got up today for this making a 24 hour mistake

The tyranny of distance . . . :wink:

sorry my friends for the rough timing! I’ll get the video up tomorrow for remote viewing

3 Likes

Hey @maxrjones , people know (apparently ??) the world is not flat so it’s always rough for someone, somewhere. Thanks for keeping the showcase rolling and recorded.

Thanks @maxrjones, much appreciated !

Thanks again for the opportunity to present at the Showcase.

The talk closed with a “Way forward” slide proposing VirtualiZarr + a forecast-specific GRIB parser (in the spirit of the HRRR-parser) to fix three problems in our GEFS / ECMWF pipeline:

  1. Zarr v2/v3: the pipeline sidestepped Zarr entirely.
  2. No Zarr codec integration: read Parquet with pandas, pulled chunks with obstore, and decoded with cfgrib via temp files: a manual, “Zarr-free” process with no native metadata, codecs or chunking.
  3. Performance — cfgrib cost ~120–470 ms per chunk because of temp-file I/O, where an in-memory Rust decoder should be far faster.

What we did instead: parquet references → Icechunk

Rather than write a VirtualiZarr parser, we converted our existing custom Parquet reference files based on kerchunk (the [url, offset, length] triplets pointing into the public GRIB archives) directly into Icechunk virtual stores, and registered gribberish as a Zarr v3 codec.

That combination turned out to close all three gaps on its own:

Problem from the slide Status now
Zarr v2/v3, no native store :white_check_mark: It is a Zarr v3 store — xr.open_zarr(..., zarr_format=3)
No codec integration :white_check_mark: gribberish is a registered Zarr v3 codec; GRIB2 decode happens transparently inside the chunk pipeline. Omit the import and you get UnknownCodecError: 'gribberish'
cfgrib temp-file cost :white_check_mark: In-memory Rust decode, ~1 s for a full 721×1440 field, no temp files, works with xarray/dask

So ds["tp"].isel(...) now issues an S3 byte-range read and decodes through the codec natively. No pandas, no temp files.

A caveat, to be precise: Icechunk and VirtualiZarr are not really alternatives, they sit on different sides. Icechunk is the store layer, and that’s what delivered the wins above. VirtualiZarr is one way to build virtual references. Because our kerchunk-based reference builder already existed, the VirtualiZarr parser was simply not on our critical path. Its promised benefit, collapsing a large bespoke reference-builder into a small standard parser is a real one we have not captured, and remains attractive on the build side. Perhaps there is thinking to the grib-index-kerchunk method is more suitable to integrate with herbie in long term.

GEFS/IFS Icechunk virtual datasets (thanks to source.coop!)

Both stores are published on Source Cooperative for hosting the space.

:link: https://source.coop/e4drr-project/forecasts

The chunks are virtual: only the store metadata lives on source.coop, while the data bytes stay in the public AWS buckets (noaa-gefs-pds, ecmwf-forecasts).

Readers need no credentials at all.

product group(s) members × steps dates (00z)
NOAA GEFS 0p25/00z 30 × 81 (0–240 h / 3 h) 2031 (2020-09-23 → 2026-04-15)
ECMWF IFS ENS 0p4/00z, 49r1/00z, 50r1/00z 51 × 85 401 / 794 / 51

ECMWF is split into three groups because the open-data schema changed eras
(0.4° 9-level → 0.25° 13-level → 0.25° 14-level dual-stream).

On “how big is it” — three different numbers

This one surprised us, so it’s worth stating plainly:

measure GEFS ECMWF
store objects on source.coop 5.4 GB 15 GB
referenced GRIB (packed) ~92 TB ~620 TB
dense float32 (ds.nbytes) 697 TB 2.79 PB

ds.nbytes — the “Size: 697TB” xarray prints — is the dense, decoded, float32 size, assuming every cell exists uncompressed. It overstates the real data volume by roughly 4.5–7×. The honest number is the referenced GRIB: the bytes you’d actually pull if you read the whole store once.

Opening the store

Self-contained, no credentials. Swap PRODUCT to switch between the two stores:

# uv run --with 'icechunk>=2.1' --with 'zarr>=3.2' --with 'xarray>=2025.1' \
#        --with 'gribberish>=1.4' --with s3fs --with numpy python
import icechunk
import numpy as np
import xarray as xr
import gribberish.zarr          # registers the "gribberish" Zarr v3 codec

STORES = {
    "gefs":  dict(prefix="forecasts/noaa_gefs_aws_s3_icechunk_vd",
                  group="0p25/00z", container="s3://noaa-gefs-pds/"),
    "ecmwf": dict(prefix="forecasts/ecmwf_ifs_ens_aws_s3_icechunk_vd",
                  group="50r1/00z", container="s3://ecmwf-forecasts/"),
}

PRODUCT = "gefs"                # <-- swap to "ecmwf"
cfg = STORES[PRODUCT]

storage = icechunk.s3_storage(
    bucket="e4drr-project", prefix=cfg["prefix"],
    endpoint_url="https://data.source.coop", region="us-east-1",
    anonymous=True,             # public read of the store metadata
    from_env=False,             # ignore any stray AWS_* env vars
    force_path_style=True,      # source.coop needs path-style addressing
)

# authorize anonymous byte-range reads of the virtual chunks on AWS
auth = icechunk.containers_credentials(
    {cfg["container"]: icechunk.s3_anonymous_credentials()})

# source.coop returns occasional transient 5xx; skip icechunk's eager
# manifest prefetch so a single bad GET doesn't fail the open
rc = icechunk.RepositoryConfig.default()
rc.manifest = icechunk.ManifestConfig(
    preload=icechunk.ManifestPreloadConfig(max_total_refs=0, max_arrays_to_scan=0))

repo = icechunk.Repository.open(
    storage, config=rc, authorize_virtual_chunk_access=auth)

ds = xr.open_zarr(repo.readonly_session("main").store, group=cfg["group"],
                  consolidated=False, zarr_format=3)
print(ds)

Some caveats

  • import gribberish.zarr is mandatory before reading anything — it registers
    the codec. Without it even opening a group fails.
  • Arrays live under a group (0p25/00z, 50r1/00z), never the root. Opening
    the root returns an empty dataset.
  • Turn off eager manifest preload on source.coop (as above). Icechunk otherwise
    prefetches thousands of manifests in parallel, and source.coop’s occasional
    transient 500s then fail the open. Reads themselves retry fine.
  • Manifest memory scales with array chunk count. Reading any chunk loads that
    array’s whole manifest (~200 bytes/ref). Our ECMWF 49r1/t has
    794 dates × 51 members × 85 steps × 13 levels ≈ 44.7 M refs ≈ 9 GB, which
    OOMs on a small machine. icechunk.ManifestSplittingConfig (splitting along
    time at write time) is the fix

Code

Scripts, smoke tests and docs (anonymous open + a per-era coverage/verification
suite) are here:

grib-index-kerchunk/icechunk-dask at main · icpac-igad/grib-index-kerchunk · GitHub

Requesting community discussion and verification on this.

1 Like

for reference, zarr-python allows you to use entrypoints to register codecs. If gribberish were to register its zarr codec that way, you could skip the import (you just need to make sure it is installed).

1 Like